//
//  SEGProtectCode.m
//  signtest
//
//  Created by wangmm on 2020/3/9.
//  Copyright © 2020 mac. All rights reserved.
//

#import "SEGProtectCode.h"
#import "SEGSecurityManager.h"

#import <sys/sysctl.h>
#import <dlfcn.h>
//#import "fishhook.h"

#import <mach-o/loader.h>
#import <mach-o/dyld.h>
// ARM and x86_64 are the only architecture that use cpu-sub-types
#define CPU_SUBTYPES_SUPPORTED  ((__arm__ || __arm64__ || __x86_64__) && !TARGET_IPHONE_SIMULATOR)

#if __LP64__
#define LC_SEGMENT_COMMAND        LC_SEGMENT_64
#define LC_SEGMENT_COMMAND_WRONG LC_SEGMENT
#define LC_ENCRYPT_COMMAND        LC_ENCRYPTION_INFO
#define macho_segment_command    segment_command_64
#define macho_section            section_64

#define macho_header            mach_header_64
#else
#define macho_header            mach_header

#define LC_SEGMENT_COMMAND        LC_SEGMENT
#define LC_SEGMENT_COMMAND_WRONG LC_SEGMENT_64
#define LC_ENCRYPT_COMMAND        LC_ENCRYPTION_INFO_64
#define macho_segment_command    segment_command
#define macho_section            section
#endif


#ifndef    _SYS_PTRACE_H_
#define    _SYS_PTRACE_H_
#include <sys/appleapiopts.h>
#include <sys/cdefs.h>
enum {
    ePtAttachDeprecated __deprecated_enum_msg("PT_ATTACH is deprecated. See PT_ATTACHEXC") = 10
};
#define    PT_TRACE_ME    0    /* child declares it's being traced */
#define    PT_READ_I    1    /* read word in child's I space */
#define    PT_READ_D    2    /* read word in child's D space */
#define    PT_READ_U    3    /* read word in child's user structure */
#define    PT_WRITE_I    4    /* write word in child's I space */
#define    PT_WRITE_D    5    /* write word in child's D space */
#define    PT_WRITE_U    6    /* write word in child's user structure */
#define    PT_CONTINUE    7    /* continue the child */
#define    PT_KILL        8    /* kill the child process */
#define    PT_STEP        9    /* single step the child */
#define    PT_ATTACH    ePtAttachDeprecated    /* trace some running process */
#define    PT_DETACH    11    /* stop tracing a process */
#define    PT_SIGEXC    12    /* signals as exceptions for current_proc */
#define PT_THUPDATE    13    /* signal for thread# */
#define PT_ATTACHEXC    14    /* attach to running process with signal exception */
#define    PT_FORCEQUOTA    30    /* Enforce quota for root */
#define    PT_DENY_ATTACH    31
#define    PT_FIRSTMACH    32    /* for machine-specific requests */
__BEGIN_DECLS
//int    segPtraceForApp(int _request, pid_t _pid, caddr_t _addr, int _data);
typedef int (*segPtraceForApp)(int _request, pid_t _pid, caddr_t _addr, int _data);
__END_DECLS
#endif    /* !_SYS_PTRACE_H_ */

@implementation SEGProtectCode
+(void)load
{
    //交换(破解sysctl)
    //[SEGProtectCode seg_breakSysctl];
    //攻破ptrace
    //[SEGProtectCode seg_breakPtrace];

    [SEGProtectCode seg_checkMachO_Restrict];
}

#pragma mark - 防重签名
/**
@brief 在启动时校验描述文件信息与打包时是否一致来判断是否被重签名
@discussion 判断组织单位和包名是否一致,查看证书里的信息有组织单位
@param signId 组织单位
@param bundleID 包名
@remark 如果被重签名则退出程序
*/
+(void)seg_jianchaSignForProtectWithCodeSignId:(NSString *)signId andBundleID:(NSString *)bundleID
{
    //embedded.mobileprovision文件中的bundleID有可能会和APP的bundleID不一样,所以两者都要检查。
    NSString * bundleIdCurrent = [SEGProtectCode seg_getBundleID];
    if (![bundleIdCurrent isEqualToString:bundleID]) {
        //检查包名
        
        //[SEGSecurityManager shared].tip = [NSString stringWithFormat:@"重签 ID不对 bundleID=%@ bundleIdCurrent=%@", bundleID, bundleIdCurrent];
        exit(0);
    }else {
        //检查embedded文件中的包名和组织名
        BOOL correct = [SEGProtectCode seg_compareCodeSignId:signId andBundleID:bundleID];
        if (!correct) {

            //[SEGSecurityManager shared].tip = @"重签 包名不对";
            exit(0);
        }
    }
}

/**
 @brief 在启动时校验描述文件信息(application-identifier)与打包时是否一致
 @discussion 判断组织单位和包名是否一致,查看证书里的信息有组织单位,例子application-identifier:3BV69E6B6Q.com.ylwy.uhomebk
 @param signId 组织单位
 @param bundleID 包名
 @return 是否相同
 @remark 返回yes表示没有重签名，返回No表示APP已经被重签名
 */
+(BOOL)seg_compareCodeSignId:(NSString *)signId andBundleID:(NSString *)bundleID {
    // 描述文件路径
    NSString *embeddedPath = [[NSBundle mainBundle] pathForResource:@"embedded" ofType:@"mobileprovision"];
    // 读取application-identifier  注意描述文件的编码要使用:NSASCIIStringEncoding
    NSString *embeddedProvisioning = [NSString stringWithContentsOfFile:embeddedPath encoding:NSASCIIStringEncoding error:nil];
    NSArray *embeddedProvisioningLines = [embeddedProvisioning componentsSeparatedByCharactersInSet:[NSCharacterSet newlineCharacterSet]];
    
    for (int i = 0; i < embeddedProvisioningLines.count; i++) {
        if ([embeddedProvisioningLines[i] containsString:@"application-identifier"]) {
            
            NSInteger fromPosition = [embeddedProvisioningLines[i+1] rangeOfString:@"<string>"].location+8;
            
            NSInteger toPosition = [embeddedProvisioningLines[i+1] rangeOfString:@"</string>"].location;
            
            NSRange range;
            range.location = fromPosition;
            range.length = toPosition - fromPosition;
            
            NSString *fullIdentifier = [embeddedProvisioningLines[i+1] substringWithRange:range];
//            NSString * signIdAndBundleIdString = [NSString stringWithFormat:@"%@.%@",signId,bundleID];
//            if (![fullIdentifier isEqualToString:signIdAndBundleIdString]) {
//                return NO;
//            }
            
            NSArray *identifierComponents = [fullIdentifier componentsSeparatedByString:@"."];
            NSString *appIdentifier = [identifierComponents firstObject];
            
            // 对比签名ID
            if (![appIdentifier isEqual:signId]) {
                return NO;
            }
            //
            //               NSMutableArray * newArray = [[NSMutableArray alloc] initWithArray:identifierComponents];
            //               [newArray removeObjectAtIndex:0];
            //               NSString * bundleIdString = [newArray componentsJoinedByString:@"."];
            //               // 对比bundleID
            //               if (![bundleIdString isEqual:bundleID]) {
            //                   return NO;
            //               }
            break;
        }
    }
    return YES;
}


+(NSString*)seg_getBundleID
{
    return [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleIdentifier"];
}
#pragma mark - sysctl检测是否被调试
/**
 @brief sysctl检测是否被调试
 @discussion 反动态调试
 @code
 if (seg_checkDebugger()) {exit(0);}
 @endcode
 @return 是否被调试
 @remark 如果被调试则退出程序，参照内嵌代码
 */
bool seg_checkDebugger(){
    //控制码
    int name[4];//放字节码-查询信息
    name[0] = CTL_KERN;//内核查看
    name[1] = KERN_PROC;//查询进程
    name[2] = KERN_PROC_PID; //通过进程id查进程
    name[3] = getpid();//拿到自己进程的id
    //查询结果
    struct kinfo_proc info;//进程查询信息结果
    size_t info_size = sizeof(info);//结构体大小
    int error = sysctl(name, sizeof(name)/sizeof(*name), &info, &info_size, 0, 0);
    assert(error == 0);//0就是没有错误
    
    //结果解析 p_flag的第12位为1就是有调试
    //p_flag 与 P_TRACED =0 就是有调试
    return ((info.kp_proc.p_flag & P_TRACED) !=0);
}

+(void)seg_checkSysctlDebugger
{
    if (seg_checkDebugger()) {
        //[SEGSecurityManager shared].tip = @"检测到动态调试";
        exit(0);
    }
}

#pragma mark - 破解sysctl，在load方法中交换，使用fishhook类
//原始函数的地址
int (*sysctl_p)(int *, u_int, void *, size_t *, void *, size_t);
//自定义函数,破解sysctl
int mySysctl(int *name, u_int namelen, void *info, size_t *infosize, void *newinfo, size_t newinfosize){
    if (namelen == 4
        && name[0] == CTL_KERN
        && name[1] == KERN_PROC
        && name[2] == KERN_PROC_PID
        && info
        && (int)*infosize == sizeof(struct kinfo_proc))
    {
        int err = sysctl_p(name, namelen, info, infosize, newinfo, newinfosize);
        //拿出info做判断
        struct kinfo_proc * myInfo = (struct kinfo_proc *)info;
        if((myInfo->kp_proc.p_flag & P_TRACED) != 0){
            //使用异或取反
            myInfo->kp_proc.p_flag ^= P_TRACED;
        }
        return err;
    }
    return sysctl_p(name, namelen, info, infosize, newinfo, newinfosize);
}

+(void)seg_breakSysctl
{
    //交换(破解sysctl)
//    rebind_symbols((struct rebinding[1]){{"sysctl",mySysctl,(void *)&sysctl_p}}, 1);
}

#pragma mark - 反修改MachO中的restrict段,在load方法中
static bool hasRestrictedSegment(const struct macho_header* mh)
{
    const uint32_t cmd_count = mh->ncmds;
    const struct load_command* const cmds = (struct load_command*)(((char*)mh)+sizeof(struct macho_header));
    const struct load_command* cmd = cmds;
    for (uint32_t i = 0; i < cmd_count; ++i) {
        switch (cmd->cmd) {
            case LC_SEGMENT_COMMAND:
            {
                const struct macho_segment_command* seg = (struct macho_segment_command*)cmd;
                printf("segmentname=%s\n", seg->segname);
                //dyld::log("seg name: %s\n", seg->segname);
                if (strcmp(seg->segname, "__RESTRICT") == 0) {
                    const struct macho_section* const sectionsStart = (struct macho_section*)((char*)seg + sizeof(struct macho_segment_command));
                    const struct macho_section* const sectionsEnd = &sectionsStart[seg->nsects];
                    for (const struct macho_section* sect=sectionsStart; sect < sectionsEnd; ++sect) {
                        if (strcmp(sect->sectname, "__restrict") == 0)
                            return true;
                    }
                }
            }
                break;
        }
        cmd = (const struct load_command*)(((char*)cmd)+cmd->cmdsize);
    }
    return false;
}

+(void)seg_checkMachO_Restrict
{
    if (@available(iOS 10.0, *)) {
        //dyld加载过程不再检测__RESTRICT段,手动的检测DYLD_INSERT_LIBRARIES环境变量.通过函数可查看当前进程环境变量的值.
        //在没有插入动态库时,env为null.
        char *env = getenv("DYLD_INSERT_LIBRARIES");
        //NSLog(@"%s",env);//一旦为自己的应用写入插件时,就可以看到控制台的输出
        if (env) {
            //有动态插入
            [SEGSecurityManager shared].isDynamicInjection = YES;
        }
    }else {
        // 获取 MachO 的 imageHeader
        // dyld 启动 App 的时候, 最先加载的是自己的 MachO, index = 0(也可以 LLDB: image list 来查看)
        //但是新版的dyld源码中去掉了__RESTRICT检测.从iOS10开始,这种防护手段已失效
//        const struct mach_header *mach_header = _dyld_get_image_header(0);
//        //mach_header->filetype
//        if(hasRestrictedSegment(mach_header)) {
//            //防护成功
//        }else{
//            //防护被攻破, rescrict二进制被改
//            [SEGSecurityManager shared].isDynamicInjection = YES;
//        }
    }
    
}

//bool HKCheckWhitelist(){
//
//    int count = _dyld_image_count();
//    for (int i = 0; i < count; i++) {
//        //遍历拿到库名称！
//       const char * imageName = _dyld_get_image_name(i);
//        //判断是否在白名单内,应用本身的路径是不确定的,所以要除外.
//        //其中libraries变量是<q style="box-sizing: border-box;">白名单</q>.
//        if (!strstr(libraries, imageName)&&!strstr(imageName, "/var/mobile/Containers/Bundle/Application")) {
//            printf("该库非白名单之内！！\n%s",imageName);
//            return NO;
//        }
//    }
//    return YES;
//}

#pragma mark - 防止xcode调试,lldb调试,debugserver调试,一般用在程序开始地方
+(void)seg_ptrace
{
    /*
     int _request: ptrace 要做的事
     pid_t _pid  : 进程 id
     caddr_t _addr:地址 默认写0
     int _data     :数据, 默认写0
     */
    void *handle = dlopen(0, RTLD_GLOBAL | RTLD_NOW);
    segPtraceForApp segPtrace_ptr = (segPtraceForApp)dlsym(handle, "ptrace");
    segPtrace_ptr(PT_DENY_ATTACH, 0, 0, 0);
}


#pragma mark - 攻破ptrace - 使用fishhook,在load方法中交换
// 定义函数指针. 保存原来函数地址
int (* seg_ptrace_p) (int _request, pid_t _pid, caddr_t _addr, int _data);
// 定义新的函数
int myPtrace (int _request, pid_t _pid, caddr_t _addr, int _data){
    if(_request != PT_DENY_ATTACH){
        return myPtrace(_request, _pid, _addr, _data);
    }
    // 如果拒绝加载, 破坏此防护
    return 0;
}

+(void)seg_breakPtrace
{
//    //攻破ptrace
//    struct rebinding ptraceBD; //
//    ptraceBD.name = "ptrace";  // 函数符号
//    ptraceBD.replacement = myPtrace; // 新函数地址
//    ptraceBD.replaced = (void *)&seg_ptrace_p; // 原始函数地址的指针
//
//    // 创建数组
//    struct rebinding rebinds[]={ptraceBD};
//    // 重绑定
//    rebind_symbols(rebinds, 1);
}
@end
